﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;

using Supremacy.Annotations;
using Supremacy.Types;

namespace SupremacyEditor
{
    public interface IEditorItem : IEditableObject,
                                   IRevertibleChangeTracking,
                                   IHeaderedItem,
                                   INotifyChanged,
                                   INotifyPropertyChanged,
                                   INotifyPropertyChanging {}

    public abstract class EditorItemBase<TBaseItem> : IEditorItem
        where TBaseItem : class
    {
        #region Fields
        private static readonly Dictionary<string, Delegate> _getAccessors;
        private static readonly Dictionary<string, Delegate> _setAccessors;

        private readonly StateScope _changedEventScope;
        private readonly TBaseItem _baseItem;
        private readonly Dictionary<string, string> _memberNames;
        private readonly Dictionary<string, object> _temporaryValues;
        private bool _isChanged;
        private bool _isEditInProgress;
        #endregion

        #region Constructors
        static EditorItemBase()
        {
            _getAccessors = new Dictionary<string, Delegate>();
            _setAccessors = new Dictionary<string, Delegate>();
        }

        protected EditorItemBase([NotNull] TBaseItem baseItem)
        {
            if (baseItem == null)
                throw new ArgumentNullException("baseItem");

            _baseItem = baseItem;
            _temporaryValues = new Dictionary<string, object>();
            _memberNames = new Dictionary<string, string>();
            _changedEventScope = new StateScope();
        }
        #endregion

        #region Properties and Indexers
        protected TBaseItem BaseItem
        {
            get { return _baseItem; }
        }
        #endregion

        #region Methods
        protected virtual void CommitChangesOverride(CommitChangesOperation commit) {}

        protected TValue GetValue<TValue>([NotNull] Expression<Func<TValue>> localAccessor)
        {
            if (localAccessor == null)
                throw new ArgumentNullException("localAccessor");

            string key = localAccessor.ToString();

            if (IsEditInProgress)
            {
                object temporaryValue;
                if (_temporaryValues.TryGetValue(key, out temporaryValue) && (temporaryValue is TValue))
                    return (TValue)temporaryValue;
            }

            return ((Func<TBaseItem, TValue>)_getAccessors[key])(this.BaseItem);
        }

        protected void OnPropertyChanged([NotNull] string propertyName)
        {
            if (propertyName == null)
                throw new ArgumentNullException("propertyName");

            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));

            if (_changedEventScope.IsWithin)
                return;

            OnChanged();
        }

        protected void OnPropertyChanged([NotNull] Expression<Func<object>> propertyAccessor)
        {
            if (propertyAccessor == null)
                throw new ArgumentNullException("propertyAccessor");

            var memberExpression = propertyAccessor.Body as MemberExpression;

            if (memberExpression == null)
                throw new ArgumentException("must be a MemberExpression", "propertyAccessor");

            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(memberExpression.Member.Name));

            OnChanged();
        }

        protected void OnPropertyChanging([NotNull] string propertyName)
        {
            if (propertyName == null)
                throw new ArgumentNullException("propertyName");

            if (PropertyChanging != null)
                PropertyChanging(this, new PropertyChangingEventArgs(propertyName));
        }

        protected void OnPropertyChanging([NotNull] Expression<Func<object>> propertyAccessor)
        {
            if (propertyAccessor == null)
                throw new ArgumentNullException("propertyAccessor");

            var memberExpression = propertyAccessor.Body as MemberExpression;

            if (memberExpression == null)
                throw new ArgumentException("must be a MemberExpression", "propertyAccessor");

            if (PropertyChanging != null)
                PropertyChanging(this, new PropertyChangingEventArgs(memberExpression.Member.Name));
        }

        protected void RegisterAccessor<T>(
            [NotNull] Expression<Func<T>> localAccessor,
            [NotNull] Func<TBaseItem, T> getAccessor,
            [NotNull] Action<TBaseItem, T> setAccessor)
        {
            if (localAccessor == null)
                throw new ArgumentNullException("localAccessor");
            if (getAccessor == null)
                throw new ArgumentNullException("getAccessor");
            if (setAccessor == null)
                throw new ArgumentNullException("setAccessor");

            var memberExpression = localAccessor.Body as MemberExpression;

            if (memberExpression == null)
                throw new ArgumentException("must be a MemberExpression", "localAccessor");

            _memberNames[localAccessor.ToString()] = memberExpression.Member.Name;
            _getAccessors[localAccessor.ToString()] = getAccessor;
            _setAccessors[localAccessor.ToString()] = setAccessor;
        }

        protected void SetValue<TValue>([NotNull] Expression<Func<TValue>> localAccessor, TValue value)
        {
            if (localAccessor == null)
                throw new ArgumentNullException("localAccessor");

            string key = localAccessor.ToString();

            OnPropertyChanging(_memberNames[key]);

            if (IsEditInProgress)
            {
                _temporaryValues[key] = value;
                _isChanged = true;
            }
            else
            {
                ((Action<TBaseItem, TValue>)_setAccessors[key])(this.BaseItem, value);
            }

            OnPropertyChanged(_memberNames[key]);
        }
        #endregion

        protected bool IsEditInProgress
        {
            get { return _isEditInProgress; }
        }

        #region CommitChangesOperation Nested Type
        protected sealed class CommitChangesOperation
        {
            #region Fields
            private readonly List<string> _errorMessages;
            #endregion

            #region Constructors
            public CommitChangesOperation()
            {
                _errorMessages = new List<string>();
            }
            #endregion

            #region Properties and Indexers
            public bool IsAborted { get; private set; }

            public ReadOnlyCollection<string> ErrorMessages
            {
                get { return new ReadOnlyCollection<string>(_errorMessages); }
            }
            #endregion

            #region Methods
            public void Abort()
            {
                if (IsAborted)
                    IsAborted = true;
            }

            public void AddErrorMessage(string message)
            {
                if (string.IsNullOrEmpty(message))
                    throw new ArgumentException("mustbe a non-null, non-empty string", "message");
                _errorMessages.Add(message);
            }
            #endregion
        }
        #endregion

        #region IEditorItem Members
        /// <summary>
        /// Begins an edit on an object.
        /// </summary>
        public void BeginEdit()
        {
            if (IsEditInProgress)
                return;

            lock (this)
            {
                if (IsEditInProgress)
                    return;

                OnBeginEdit();

                _isEditInProgress = true;
            }
        }

        /// <summary>
        /// Pushes changes since the last <see cref="BeginEdit" /> call into the underlying object.
        /// </summary>
        public void EndEdit()
        {
            if (!IsEditInProgress)
                return;

            lock (this)
            {
                if (!IsEditInProgress)
                    return;

                var commit = new CommitChangesOperation();

                CommitChangesOverride(commit);

                if (commit.IsAborted)
                    return;

                foreach (var localAccessor in _temporaryValues.Keys)
                {
                    _setAccessors[localAccessor].DynamicInvoke(
                        this.BaseItem,
                        _temporaryValues[localAccessor]);
                }

                _temporaryValues.Clear();

                _isEditInProgress = false;
                _isChanged = false;
            }
        }

        /// <summary>
        /// Discards changes since the last <see cref="M:System.ComponentModel.IEditableObject.BeginEdit" /> call.
        /// </summary>
        public void CancelEdit()
        {
            if (!IsEditInProgress)
                return;

            lock (this)
            {
                if (!IsEditInProgress)
                    return;

                foreach (var localAccessor in _temporaryValues.Keys.ToList())
                {
                    var memberName = _memberNames[localAccessor];
                    OnPropertyChanging(memberName);
                    _temporaryValues.Remove(localAccessor);
                    OnPropertyChanged(memberName);
                }

                OnCancelEdit();

                _isEditInProgress = false;
                _isChanged = false;
            }
        }

        /// <summary>
        /// Resets the object’s state to unchanged by accepting the modifications.
        /// </summary>
        public void AcceptChanges()
        {
            EndEdit();
        }

        /// <summary>
        /// Gets the object's changed status.
        /// </summary>
        /// <returns>
        /// <c>true</c> if the object’s content has changed since the last call to 
        /// <see cref="AcceptChanges" />; otherwise, <c>false</c>.
        /// </returns>
        public bool IsChanged
        {
            get { return _isChanged || IsChangedOverride(); }
        }

        /// <summary>
        /// Resets the object’s state to unchanged by rejecting the modifications.
        /// </summary>
        public void RejectChanges()
        {
            CancelEdit();
        }

        public abstract string Header { get; }

        public event PropertyChangedEventHandler PropertyChanged;
        public event PropertyChangingEventHandler PropertyChanging;

        public event EventHandler<EventArgs> Changed;
        #endregion

        protected virtual void OnBeginEdit() {}
        protected virtual void OnCancelEdit() {}

        protected virtual bool IsChangedOverride()
        {
            return false;
        }

        protected void OnChanged()
        {
            if (_changedEventScope.IsWithin)
                return;
            using (_changedEventScope.Enter())
            {
                var handler = this.Changed;
                if (handler != null)
                    handler(this, EventArgs.Empty);

                OnPropertyChanged("IsChanged");
            }
        }
    }
}